package email.core;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.mail.Address;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Header;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.UIDFolder;
import javax.mail.Flags.Flag;
import javax.mail.Message.RecipientType;
import javax.mail.internet.MimeUtility;

import com.sun.mail.pop3.POP3Folder;

public class EmailChecker {
	final Logger LOGGER = Logger.getLogger(this.getClass().getName());
	public enum Protocol { 
		POP3("pop3", 110, false), IMAP("imap", 143, false), POP3_SSL("pop3", 995, true), IMAP_SSL("imap", 993, true);  
		String name; 
		int port;
		boolean isSsl;
		Protocol(String name, int port,boolean isSsl) {this.name = name; this.port = port; this.isSsl = isSsl; }
		public String getName() {return this.name; }
		public int getPort() {return this.port; }
		public boolean getSSL() {return this.isSsl; }
	}
	static final String SSL_FACTORY_CLASS_NAME = "javax.net.ssl.SSLSocketFactory";
	static final String PROPERTY_SOCKET_FACTORY = "mail..socketFactory.class"; 
	static final String PROPERTY_STORE_PROTOCOL = "mail.store.protocol"; 
	static final String PROPERTY_HOST_DEFAULT = "mail.host";
	static final String PROPERTY_PORT_PROTOCOL = "mail..port";
	static final String PROPERTY_SOCKET_FACTORY_PORT_PROTOCOL = "mail..socketFactory.port";
	static final String PROPERTY_USER_DEFAULT = "mail.user"; // Is it to be used at all?
	static final String PROPERTY_USE_POP3_RETR_HEADERS = "mail.pop3.forgettopheaders";
	static final String FOLDER_NAME_INBOX = "inbox";
	static final String PROPERTY_SOCKET_FACTORY_PROVIDER = "ssl.SocketFactory.provider";
	
	String mailboxName;
	Protocol protocol = Protocol.IMAP;
	String userName;
	String password;
	String targetHost;
	Integer targetPort;
	Boolean useSSL;
	Boolean useRetrHeaders;
	MessageListCallback callback;
	Thread checkThread;
	
	public interface MessageListCallback {
		void onStatusChange(String statusMessage);
		void onMessageList(List<EmailMessage> messages);
		void onError(String errorMessage);
	}

	public String getMailboxName() {
		return mailboxName;
	}

	public void setMailboxName(String mailboxName) {
		this.mailboxName = mailboxName;
	}

	public Protocol getProtocol() {
		return protocol;
	}

	public void setProtocol(Protocol protocol) {
		this.protocol = protocol;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getTargetHost() {
		return targetHost;
	}

	public void setTargetHost(String targetHost) {
		this.targetHost = targetHost;
	}

	public Integer getTargetPort() {
		return targetPort;
	}

	public void setTargetPort(Integer targetPort) {
		this.targetPort = targetPort;
	}

	public Boolean getUseSSL() {
		return useSSL;
	}

	public void setUseSSL(Boolean useSSL) {
		this.useSSL = useSSL;
	}

	public MessageListCallback getCallback() {
		return callback;
	}

	public void setCallback(MessageListCallback callback) {
		this.callback = callback;
	}

	public Boolean getUseRetrHeaders() {
		return useRetrHeaders;
	}

	public void setUseRetrHeaders(Boolean useRetrHeaders) {
		this.useRetrHeaders = useRetrHeaders;
	}

	synchronized public void loadMessagesList(final List<String> deleteUIDs, final List<String> loadUIDs) {
		if (checkThread!=null) { checkThread.interrupt(); checkThread = null; }
		// Run messages loading in a thread
		checkThread = new Thread() {
			
			void checkInterrupted() throws InterruptedException {
				Thread.yield();
				if (this.isInterrupted()) {
					throw new InterruptedException();
				}
			}
			
			@Override
			public void run() {
				Store st = null;
				Folder f = null;
				try {
					checkInterrupted();
					if (callback!=null) { callback.onStatusChange(ConfigurationProvider.getInstance().getMessageString("check.status.init")); }
					Session s = Session.getInstance(getSessionProperties(), null);
					checkInterrupted();
					if (callback!=null) { callback.onStatusChange(ConfigurationProvider.getInstance().getMessageString("check.status.session_created")); }
					try {
						checkInterrupted();
						if (callback!=null) { callback.onStatusChange(ConfigurationProvider.getInstance().getMessageString("check.status.retrieving_store")); }
						st = s.getStore();
						if (callback!=null) { callback.onStatusChange(ConfigurationProvider.getInstance().getMessageString("check.status.store_retrieved")); }
					} catch (NoSuchProviderException e) {
						if (callback!=null) {callback.onError(ConfigurationProvider.getInstance().getMessageString("check.status.store_retrieval_failure")); }
						return;
					}
					try {
						checkInterrupted();
						if (callback!=null) { callback.onStatusChange(ConfigurationProvider.getInstance().getMessageString("check.status.connecting_server")); }
						st.connect(userName, password);
						checkInterrupted();
						if (callback!=null) { callback.onStatusChange(ConfigurationProvider.getInstance().getMessageString("check.status.retrieving_inbox")); }
						f = st.getFolder("Inbox");
						checkInterrupted();
						boolean checkDelete = false;
						String message = ConfigurationProvider.getInstance().getMessageString("check.status.loading_messages");
						if (deleteUIDs!=null && !deleteUIDs.isEmpty()) {
							message = ConfigurationProvider.getInstance().getMessageString("check.status.deleting_messages");
							checkDelete = true;
						}
						if (callback!=null) { callback.onStatusChange(ConfigurationProvider.getInstance().getMessageString("check.status.opening_inbox")); }
						f.open(checkDelete?Folder.READ_WRITE:Folder.READ_ONLY);
						checkInterrupted();
						if (callback!=null) { callback.onStatusChange(ConfigurationProvider.getInstance().getMessageString("check.status.retrieving_messages")); }
						Message[] m = f.getMessages();
						checkInterrupted();
						if (callback!=null) { callback.onStatusChange(ConfigurationProvider.getInstance().getMessageString("check.status.messages_list")); }
						if (callback!=null) { callback.onStatusChange(message); }
						List<EmailMessage> currentMessages = new ArrayList<EmailMessage>(m.length);
						for (Message msg : m) {
							EmailMessage emsg;
							if (!msg.getFlags().contains(Flags.Flag.DELETED)) {
								emsg = extractEmailMessage(f, msg);
								checkInterrupted();
								if (checkDelete && deleteUIDs.contains(emsg.uuid)) {
									msg.setFlag(Flag.DELETED, true);
								} else {
									currentMessages.add(emsg);
								}
								checkInterrupted();
							}
						}
						checkInterrupted();
						if (callback!=null) { callback.onMessageList(currentMessages); }
					} catch (MessagingException e) {
						LOGGER.log(Level.WARNING, targetHost+": "+"Error during connection/messages loading: "+e.getMessage());
						if (callback!=null) {callback.onError(ConfigurationProvider.getInstance().getMessageString("check.status.error")+": "+e.getMessage()); }
						checkInterrupted();
					} finally {
						closeResources(f, st);
					}
				} catch (InterruptedException e) {
					closeResources(f, st);
					if (callback!=null) { callback.onError(ConfigurationProvider.getInstance().getMessageString("check.status.interrupted")); }
					
				}
			}
			
			void closeResources(Folder f, Store st) {
				if (f!=null && f.isOpen()) {
					try {
						f.close(true);
					} catch (MessagingException e1) {
						LOGGER.log(Level.WARNING, targetHost+": "+"Could not close INBOX folder: "+e1.getMessage());
					}
				}
				try {
					if (st!=null && st.isConnected()) { st.close(); }
				} catch (MessagingException e1) {
					LOGGER.log(Level.WARNING, targetHost+": "+"Could not close Session Store: "+e1.getMessage());
				}
			}
			
			EmailMessage extractEmailMessage(Folder f, Message msg) throws InterruptedException {
				EmailMessage emailMessage = new EmailMessage();
				emailMessage.mailbox = mailboxName;
				emailMessage.uuid = "";
				if (f instanceof UIDFolder) {
					try {
						checkInterrupted();
						emailMessage.uuid = ""+((UIDFolder)f).getUID(msg);
					} catch (MessagingException e) {
						e.printStackTrace();
					}
				}
				if (f instanceof POP3Folder) {
					try {
						checkInterrupted();
						emailMessage.uuid = ((POP3Folder)f).getUID(msg);
					} catch (MessagingException e) {
						e.printStackTrace();
					}
				}
				checkInterrupted();
				emailMessage.serverIndex = msg.getMessageNumber();
				try {
					checkInterrupted();
					emailMessage.to = decodeText(getAddressString(msg.getRecipients(RecipientType.TO)));
				} catch (MessagingException e2) {
					e2.printStackTrace();
				}
				try {
					checkInterrupted();
					emailMessage.from = decodeText(getAddressString(msg.getFrom()));
				} catch (MessagingException e) {
					e.printStackTrace();
				}
				try {
					checkInterrupted();
					emailMessage.subject = decodeText(msg.getSubject());
				} catch (MessagingException e) {
					e.printStackTrace();
				}
				try {
					checkInterrupted();
					emailMessage.date = msg.getSentDate();
				} catch (MessagingException e) {
					e.printStackTrace();
				}
				if (emailMessage.date == null) {
					try {
						checkInterrupted();
						emailMessage.date = msg.getReceivedDate();
					} catch (MessagingException e) {
						e.printStackTrace();
					}
				}
				if (emailMessage.date == null) { emailMessage.date = new Date(); }
				try {
					checkInterrupted();
					emailMessage.size = msg.getSize();
				} catch (MessagingException e) {
					e.printStackTrace();
				}
				
				try {
					checkInterrupted();
					@SuppressWarnings("unchecked")
					Enumeration<Header> headers = (Enumeration<Header>)msg.getAllHeaders();
					while(headers.hasMoreElements()) {
						checkInterrupted();
						emailMessage.headers.add(headers.nextElement());
					}
				} catch (MessagingException e) {
					e.printStackTrace();
				}
				
				return emailMessage;
			}
			
			String getAddressString(Address[] addresses) {
				String result = "";
				if (addresses!=null && addresses.length>0) { result = addresses[0].toString(); }
				return result;
			}
			
			String decodeText(String text) {
				String result = text;
				try {
					if (text != null) {result = MimeUtility.decodeText(text); } else { result = ""; }
				} catch (UnsupportedEncodingException e) {
					e.printStackTrace();
				}
				return result;
			}
			
//			String getRecipients(Address[] addresses) {
//				StringBuffer result = new StringBuffer(32);
//				for (Address address : addresses) {
//					if (result.length()>0) { result.append(";"); }
//					result.append(address);
//				}
//				return result.toString();
//			}
		};
		checkThread.start();
	}
	
	public void stopCheckThread() {
		if (checkThread!=null) {
			checkThread.interrupt();
		}
	}
	
	Properties getSessionProperties() {
		Properties result = new Properties();
		result.setProperty(PROPERTY_HOST_DEFAULT, this.targetHost);
		int targetPort = this.protocol.port;
		if (this.targetPort!=null) {
			targetPort = this.targetPort;
		} 
		result.setProperty(PROPERTY_PORT_PROTOCOL.replaceFirst("\\.\\.", "."+this.protocol.getName()+"."), ""+targetPort); 
		result.setProperty(PROPERTY_STORE_PROTOCOL, this.protocol.getName());
		// Configure headers retrieval for POP3 servers: TOP headers are default, check if RETR headers will be used instead
		// This is useful when TOP is not supported
		if (Protocol.POP3.equals(this.protocol) || Protocol.POP3_SSL.equals(this.protocol) && this.useRetrHeaders) {
			result.setProperty(PROPERTY_USE_POP3_RETR_HEADERS, "true");
		}
		// This may be not necessary since is used directly when connecting store
		result.setProperty(PROPERTY_USER_DEFAULT, this.userName); 
		if ((this.useSSL!=null && this.useSSL) || this.protocol.getSSL()) {
			result.setProperty(PROPERTY_SOCKET_FACTORY.replaceFirst("\\.\\.", "."+this.protocol.getName()+"."), SSL_FACTORY_CLASS_NAME); 
			result.setProperty(PROPERTY_SOCKET_FACTORY_PORT_PROTOCOL.replaceFirst("\\.\\.", "."+this.protocol.getName()+"."), ""+targetPort); 
		}
		return result;
	}
}
